/* Lexical scanning. */

#include "defs.h"
#include "data.h"
#include "decl.h"

/*
 * Get the next character from the input file.
 */
static int next(void)
{
    int ch;

    if (Putback) {      /* use the character put back */
        ch = Putback;   /* if there is one */
        Putback = 0;
        return ch;
    }

    ch = fgetc(Infile);     /* read from input file */
    if (ch == '\n')
        Line++;             /* increment line count */
    return ch;
}

/*
 * Put back an unwantted character.
 */
static void putback(int ch)
{
    Putback = ch;
}

/*
 * Skip past input that we don't need to deal with,
 * i.e. whitespace, newlines. Return the first 
 * character we do need to deal with.
 */
static int skip(void)
{
    int ch;

    ch = next();
    while (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\f')
        ch = next();
    return ch;
}

/*
 * Return the position of character c
 * in string s, or -1 if c not found.
 */
static int chrpos(char *s, int c)
{
    char *p;

    p = strchr(s, c);
    return (p ? p - s : -1);
}

/*
 * Scan and return an integer literal
 * value from the input file.
 */
static int scanint(int ch)
{
    int k, val = 0;

    /* convert each character into an int value */
    while ((k = chrpos("0123456789", ch)) >= 0) {
        val = val * 10 + k;
        ch = next();
    }

    /* hit a non-integer character, put it back */
    putback(ch);
    return val;
}

/*
 * Scan an identifier from the input file and
 * store it in buf[]. Return the identifier's length.
 */
static int scanident(int ch, char *buf, int limit)
{
    int idx = 0;

    /* allow digits, alpha and underscores */
    while (isalpha(ch) || isdigit(ch) || ch == '_') {
        /* error if we hit the identifier length limit,
           else append to buf[] and get next character */
        if (limit - 1 == idx) {
            printf("identifier too long on line %d\n", Line);
            exit(1);
        } else if (idx < limit - 1) {
            buf[idx++] = ch;
        }
        ch = next();
    }

    putback(ch);        /* we hit a non-valid character, put it back */
    buf[idx] = '\0';    /* NULL-terminate the buf[] */

    return idx;         /* and return the length */
}

/*
 * Given a word from input, return the matching 
 * keyword token number or 0 if it's not a keyword.
 * Switch on the first letter so that we don't have 
 * to waste time strcmp() against all keywords.
 */
static int keyword(char *str)
{
    switch (*str) {
    case 'c':
        if (!strcmp(str, "char"))
            return T_CHAR;
        break;

    case 'e':
        if (!strcmp(str, "else"))
            return T_ELSE;
        break;

    case 'f':
        if (!strcmp(str, "for"))
            return T_FOR;
        break;

    case 'i':
        if (!strcmp(str, "int"))
            return T_INT;
        if (!strcmp(str, "if"))
            return T_IF;
        break;

    case 'p':
        if (!strcmp(str, "print"))
            return T_PRINT;
        break;

    case 'v':
        if (!strcmp(str, "void"))
            return T_VOID;
        break;

    case 'w':
        if (!strcmp(str, "while"))
            return T_WHILE;
        break;

    default:
    }

    return 0;
}

/*
 * Scan and return the next token found in the input.
 * Return 1 if token valid, 0 if no token left.
 */
int scan(struct token *t)
{
    int ch, tokentype;

    ch = skip();    /* skip whitespace */

    switch (ch) {
    case EOF: t->token = T_EOF;     return 0;
    case '+': t->token = T_PLUS;    break;
    case '-': t->token = T_MINUS;   break;
    case '*': t->token = T_STAR;    break;
    case '/': t->token = T_SLASH;   break;
    case ';': t->token = T_SEMI;    break;
    case '{': t->token = T_LBRACE;  break;
    case '}': t->token = T_RBRACE;  break;
    case '(': t->token = T_LPAREN;  break;
    case ')': t->token = T_RPAREN;  break;
    case '=':
        if ((ch = next()) == '=') {
            t->token = T_EQ;
        } else {
            putback(ch);
            t->token = T_ASSIGN;
        }
        break;

    case '!':
        if ((ch = next()) == '=')
            t->token = T_NE;
        else
            fatalc("Unrecognised character", ch);

    case '<':
        if ((ch = next()) == '=') {
            t->token = T_LE;
        } else {
            putback(ch);
            t->token = T_LT;
        }
        break;

    case '>':
        if ((ch = next()) == '=') {
            t->token = T_GE;
        } else {
            putback(ch);
            t->token = T_GT;
        }
        break;

    default:
        /* If it's a digit, scan the literal integer in */
        if (isdigit(ch)) {
            t->intvalue = scanint(ch);
            t->token = T_INTLIT;
            break;
        } else if (isalpha(ch) || ch == '_') {
            /* read in a keyword or identifier */
            scanident(ch, Text, TEXTLEN);

            /* if it's a recognized keyword, return that token */
            if (tokentype = keyword(Text)) {
                t->token = tokentype;
                break;
            } else {
            /* not a recognized keyword, it must be a identifier */
                t->token = T_IDENT;
                break;
            }
        }

        fatalc("Unrecognised character", ch);
    }

    return 1;
}
